package oz.mano.online.netty.file_server;

import static io.netty.handler.codec.http.HttpHeaders.isKeepAlive;
import static io.netty.handler.codec.http.HttpHeaders.setContentLength;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONNECTION;
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE;
import static io.netty.handler.codec.http.HttpHeaders.Names.LOCATION;
import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN;
import static io.netty.handler.codec.http.HttpResponseStatus.FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
import static io.netty.handler.codec.http.HttpResponseStatus.METHOD_NOT_ALLOWED;
import static io.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelProgressiveFuture;
import io.netty.channel.ChannelProgressiveFutureListener;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.regex.Pattern;

import javax.activation.MimetypesFileTypeMap;

public class HttpFileServerHandler extends
		SimpleChannelInboundHandler<FullHttpRequest> {
	
	private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9\\.]*");
	private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
	
	private final String url;

	public HttpFileServerHandler(String url) {
		this.url = url;
	}

	@Override
	protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest req)
			throws Exception {
		if(!req.getDecoderResult().isSuccess()){
			sendError(ctx,BAD_REQUEST);
			return ;
		}
		
		if(req.getMethod()!=GET){
			sendError(ctx,METHOD_NOT_ALLOWED);
			return ;
		}
		
		final String uri = req.getUri();
		final String path = santizeUri(uri);
		
		if(null == path){
			sendError(ctx,FORBIDDEN);
			return ;
		}
		
		File file = new File(path);
		if(file.isHidden() || !file.exists()){
			sendError(ctx, NOT_FOUND);
			return ;
		}
		
		if(file.isDirectory()){
			if(uri.endsWith("/")){
				sendListing(ctx,file);
			}else{
				sendRedirect(ctx,uri+"/");
			}
			return ;
		}
		
		if(!file.isFile()){
			sendError(ctx,FORBIDDEN);
			return ;
		}
		
		RandomAccessFile randomAccessFile = null;
		try{
			randomAccessFile = new RandomAccessFile(file,"r");
		}catch(FileNotFoundException e){
			sendError(ctx,NOT_FOUND);
			return;
		}
		
		long fileLength = randomAccessFile.length();
		HttpResponse response = new DefaultHttpResponse(HTTP_1_1,OK);
		setContentLength(response,fileLength);
		setContentTypeHeader(response,file);
		
		if(isKeepAlive(req)){
			response.headers().set(CONNECTION,HttpHeaders.Values.KEEP_ALIVE);
		}
		
		ctx.write(response);
		
		ChannelFuture sendChannelFuture = null;
		// 同过netty的ChunkedFile对象直接将文件写入到发送缓冲区，最后为sendFileFeature增加GenericFeatureListener，
        // 如果发送完成，打印“Transfer complete”
		sendChannelFuture = ctx.write(new ChunkedFile(randomAccessFile,0,fileLength,8192),ctx.newProgressivePromise());
		sendChannelFuture.addListener(new ChannelProgressiveFutureListener() {

			public void operationProgressed(ChannelProgressiveFuture future,
					long progress, long total) throws Exception {
				if (total < 0) { // total unknown
                    System.err.println("Transfer progress: " + progress);
                } else {
                    System.err.println("Transfer progress: " + progress + " / "
                        + total);
                }
			}

			public void operationComplete(ChannelProgressiveFuture future)
					throws Exception {
				System.out.println("Transfer complete.");
			}
			
			
		});
		
		ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
		if(!isKeepAlive(req)){
			lastContentFuture.addListener(ChannelFutureListener.CLOSE);
		}
	}
	
	

	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
			throws Exception {
		cause.printStackTrace();
		if(ctx.channel().isActive()){
			sendError(ctx, INTERNAL_SERVER_ERROR);
		}
	}

	private void setContentTypeHeader(HttpResponse response, File file) {
		MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
		response.headers().set(CONTENT_TYPE,mimetypesFileTypeMap.getContentType(file.getPath()));
		
	}

	private void sendRedirect(ChannelHandlerContext ctx, String newUri) {
		FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, FOUND);
        response.headers().set(LOCATION, newUri);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}

	private void sendListing(ChannelHandlerContext ctx, File dir) {
		FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, OK);
		response.headers().set(CONTENT_TYPE,"text/html;charset=UTF-8");
		StringBuilder buf = new StringBuilder();
        String dirPath = dir.getPath();
        buf.append("<!DOCTYPE html>\r\n");
        buf.append("<html><head><title>");
        buf.append(dirPath);
        buf.append(" 目录：");
        buf.append("</title></head><body>\r\n");
        buf.append("<h3>");
        buf.append(dirPath).append(" 目录：");
        buf.append("</h3>\r\n");
        buf.append("<ul>");
        // 此处打印了一个 .. 的链接
        buf.append("<li>链接：<a href=\"../\">..</a></li>\r\n");
        // 用于展示根目录下的所有文件和文件夹，同时使用超链接标识
        for (File f : dir.listFiles()) {
            if (f.isHidden() || !f.canRead()) {
            continue;
            }
            String name = f.getName();
            if (!ALLOWED_FILE_NAME.matcher(name).matches()) {
            continue;
            }
            buf.append("<li>链接：<a href=\"");
            buf.append(name);
            buf.append("\">");
            buf.append(name);
            buf.append("</a></li>\r\n");
        }
        buf.append("</ul></body></html>\r\n");
        
        ByteBuf buffer = Unpooled.copiedBuffer(buf, CharsetUtil.UTF_8);
        response.content().writeBytes(buffer);
        buffer.release();
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
        
		
	}

	private void sendError(ChannelHandlerContext ctx,
			HttpResponseStatus status) {
		FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status.toString()
                + "\r\n", CharsetUtil.UTF_8));
		response.headers().set(CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	}

	private String santizeUri(String uri) {
		try {
			uri = URLDecoder.decode(uri, CharsetUtil.UTF_8.name());
		} catch (UnsupportedEncodingException e) {
			try{
				uri = URLDecoder.decode(uri,CharsetUtil.ISO_8859_1.name());
			}catch(UnsupportedEncodingException ex){
				throw new Error();
			}
		}
		
		uri = uri.replace('/',File.separatorChar);
		
		if(uri.contains(File.separator+".") || uri.contains(File.separator+".")
				|| uri.startsWith(".")
				|| uri.endsWith(".")
				|| INSECURE_URI.matcher(uri).matches()){
			return null;
		}

		return System.getProperty("user.dir")+File.separator+uri;
	}

}
